﻿//#define UNDO_DEBUG

#if UNITY_EDITOR

using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;


namespace AI.Graph.Editor {


    [Serializable]
    public class GraphUndo {

#region Static

        private static GraphUndo s_instance;
        private static Action<object, object> getRecordsInternalCallback;

        private static GraphUndo current {
            get {
                if (s_instance == null) {
                    s_instance = new GraphUndo ();
                }

                // TODO I have currently no idea how this can happen.
                else if (s_instance.m_recordObject == null) {
                    #if UNDO_DEBUG
                    Debug.Log ("[Undo]: Recorded object is null");
                    #endif
                    s_instance.m_recordObject = ScriptableObject.CreateInstance<RecordObject> ();
                }

                return s_instance;
            }
        }

#endregion


#region Fields

        [SerializeField]
        private RecordObject m_recordObject;

        [SerializeField]
        private List<RecordAction> m_undoStack = new List<RecordAction> ();

        [SerializeField]
        private List<RecordAction> m_redoStack = new List<RecordAction> ();

        [SerializeField]
        private UnityRecords m_unityRecords = new UnityRecords ();

        [SerializeField]
        private UnityRecords m_unityRecordsOnUndoRedoPerformed = new UnityRecords ();

        [SerializeField]
        private List<string> m_lastUndoRecords = new List<string> ();

        [SerializeField]
        private List<string> m_lastRedoRecords = new List<string> ();

        [SerializeField]
        private bool m_undoRedoPerformed;

#endregion


#region Properties

        public static bool hasUndo {
            get { return current.m_undoStack.Count > 0; }
        }

        public static bool hasRedo {
            get { return current.m_redoStack.Count > 0; }
        }

        public static bool hasUnityUndo {
            get { return unityUndoRecords.Length > 0; }
        }

        public static bool hasUnityRedo {
            get { return unityRedoRecords.Length > 0; }
        }

        public static string[] undoRecords {
            get {
                List<string> records = new List<string> ();

                foreach (RecordAction action in current.m_undoStack) {
                    records.Insert (0, action.name);
                }

                return records.ToArray ();
            }
        }

        public static string[] redoRecords {
            get {
                List<string> records = new List<string> ();

                foreach (RecordAction action in current.m_redoStack) {
                    records.Insert (0, action.name);
                }

                return records.ToArray ();
            }
        }

        public static string[] unityUndoRecords {
            get { return current.m_unityRecords.undoRecords.ToArray (); }
        }

        public static string[] unityRedoRecords {
            get { return current.m_unityRecords.redoRecords.ToArray (); }
        }

        public static string[] unityUndoRecordsOnUndoRedo {
            get { return current.m_unityRecordsOnUndoRedoPerformed.undoRecords.ToArray (); }
        }

        public static string[] unityRedoRecordsOnUndoRedo {
            get { return current.m_unityRecordsOnUndoRedoPerformed.redoRecords.ToArray (); }
        }

        public static string[] lastUndoRecords {
            get { return current.m_lastUndoRecords.ToArray (); }
        }

        public static string[] lastRedoRecords {
            get { return current.m_lastRedoRecords.ToArray (); }
        }

        public static event Action onRecordsChanged;

#endregion


        private GraphUndo () {
            m_recordObject = ScriptableObject.CreateInstance<RecordObject> ();

            // Get the internal delegate from Unity.
            Assembly UnityEditorAsssembly = Assembly.GetAssembly (typeof (UnityEditor.Editor));
            Type undoType = UnityEditorAsssembly.GetType ("UnityEditor.Undo");
            MethodInfo getRecordsInternal = undoType.GetMethod ("GetRecordsInternal", BindingFlags.NonPublic | BindingFlags.Static);
            getRecordsInternalCallback = (Action<object, object>) Delegate.CreateDelegate (typeof (Action<object, object>), getRecordsInternal);

            EditorApplication.update -= Update;
            EditorApplication.update += Update;

            UnityEditor.Undo.undoRedoPerformed -= OnUndoRedoPerformed;
            UnityEditor.Undo.undoRedoPerformed += OnUndoRedoPerformed;


            #if UNDO_DEBUG
            Debug.Log ("Undo Initialized");
            #endif
        }


        UnityRecords GetUnityRecords () {
            UnityRecords records = new UnityRecords ();
            getRecordsInternalCallback (records.undoRecords, records.redoRecords);
            return records;
        }


        void Update () {
            if (!m_undoRedoPerformed) {
                m_unityRecordsOnUndoRedoPerformed = m_unityRecords;
            }

            m_undoRedoPerformed = false;
            m_unityRecords = GetUnityRecords ();
        }


        void OnUndoRedoPerformed () {
            m_undoRedoPerformed = true;
            m_unityRecords = GetUnityRecords ();

            int undoDiff = m_unityRecords.undoRecords.Count - m_unityRecordsOnUndoRedoPerformed.undoRecords.Count;
            int redoDiff = m_unityRecords.redoRecords.Count - m_unityRecordsOnUndoRedoPerformed.redoRecords.Count;
            bool wasUndo = undoDiff < 0;
            bool wasRedo = undoDiff > 0;
            undoDiff = Mathf.Abs (undoDiff);
            redoDiff = Mathf.Abs (redoDiff);

            if (wasUndo) {
                #if UNDO_DEBUG
                Debug.Log ("undo performed: " + undoDiff);
                #endif

                int l = m_unityRecordsOnUndoRedoPerformed.undoRecords.Count - 1;
                m_lastUndoRecords.Clear ();

                for (int i = 0; i < undoDiff; i++) {
                    m_lastUndoRecords.Add (m_unityRecordsOnUndoRedoPerformed.undoRecords[l - i]);
                }

                OnUndoPerformed (m_lastUndoRecords);
                onRecordsChanged?.Invoke ();
            }
            else if (wasRedo) {
                #if UNDO_DEBUG
                Debug.Log ("redo performed: " + redoDiff);
                #endif

                int l = m_unityRecordsOnUndoRedoPerformed.redoRecords.Count - 1;
                m_lastRedoRecords.Clear ();

                for (int i = 0; i < redoDiff; i++) {
                    m_lastRedoRecords.Add (m_unityRecordsOnUndoRedoPerformed.redoRecords[l - i]);
                }

                OnRedoPerformed (m_lastRedoRecords);
                onRecordsChanged?.Invoke ();
            }
            #if UNDO_DEBUG
            else {
                Debug.Log ("Nothing changed.");
            }
            #endif
        }


        void OnUndoPerformed (List<string> lastUndoRecords) {
            if (m_undoStack.Count > 0) {
                if (lastUndoRecords.Contains (m_undoStack[m_undoStack.Count - 1].name)) {
                    RecordAction action = m_undoStack[m_undoStack.Count - 1];
                    m_undoStack.RemoveAt (m_undoStack.Count - 1);

                    action.Undo ();

                    m_redoStack.Add (action);
                }
            }
            #if UNDO_DEBUG
            else {
                Debug.Log ("Undo Stack is empty.");
            }
            #endif
        }


        void OnRedoPerformed (List<string> lastRedoRecords) {
            if (m_redoStack.Count > 0) {
                if (lastRedoRecords.Contains (m_redoStack[m_redoStack.Count - 1].name)) {
                    RecordAction action = m_redoStack[m_redoStack.Count - 1];
                    m_redoStack.RemoveAt (m_redoStack.Count - 1);

                    action.Redo ();

                    m_undoStack.Add (action);
                }
            }
            #if UNDO_DEBUG
            else {
                Debug.Log ("Redo Stack is empty.");
            }
            #endif
        }


        public static void RecordAndPerform (string name, Action perform, Action onUndo) {
            perform ();
            Record (name, onUndo, perform);
        }


        public static void Record (string name, Action onUndo, Action onRedo) {
            UnityEditor.Undo.IncrementCurrentGroup ();

            UnityEditor.Undo.RegisterCompleteObjectUndo (current.m_recordObject, name);

            UnityEditor.Undo.FlushUndoRecordObjects ();
            UnityEditor.Undo.IncrementCurrentGroup ();

            current.m_undoStack.Add (new RecordAction (name, onUndo, onRedo));
            current.m_redoStack.Clear ();

            onRecordsChanged?.Invoke ();

            #if UNDO_DEBUG
            Debug.Log ("New Record created.");
            #endif
        }


        public static void PerformUndo () {
            UnityEditor.Undo.PerformUndo ();
        }


        public static void PerformRedo () {
            UnityEditor.Undo.PerformRedo ();
        }


        public static void Clear () {
            current.m_undoStack.Clear ();
            current.m_redoStack.Clear ();
            current.m_lastUndoRecords.Clear ();
            current.m_lastRedoRecords.Clear ();
            UnityEditor.Undo.ClearUndo (current.m_recordObject);

            onRecordsChanged?.Invoke ();

            #if UNDO_DEBUG
            Debug.Log ("Records deleted.");
            #endif
        }


        public static void ClearAll () {
            current.m_undoStack.Clear ();
            current.m_redoStack.Clear ();
            current.m_lastUndoRecords.Clear ();
            current.m_lastRedoRecords.Clear ();
            UnityEditor.Undo.ClearAll ();

            onRecordsChanged?.Invoke ();

            #if UNDO_DEBUG
            Debug.Log ("All Records deleted");
            #endif
        }


        [Serializable] class RecordObject : ScriptableObject { }

        [Serializable]
        public class UnityRecords {

            public List<string> undoRecords = new List<string> ();
            public List<string> redoRecords = new List<string> ();

        }

        [Serializable]
        class RecordAction {

            [SerializeField]
            private string m_name;

            [SerializeField]
            private Action m_undo;

            [SerializeField]
            private Action m_redo;


            public string name {
                get { return m_name; }
            }


            public RecordAction (string name, Action undo, Action redo) {
                m_name = name;
                m_undo = undo;
                m_redo = redo;
            }


            public void Undo () {
                m_undo.Invoke ();
            }


            public void Redo () {
                m_redo.Invoke ();
            }

        }

    }


}
#endif